#ifndef _RESOURCES_
#define _RESOURCES_

#include <memory>
#include <renderware.h>
#include "math.hxx"

template <typename dispatchType>
struct ResourceManager
{
private:
    struct eirFileSystemMetaInfo
    {
        inline eirFileSystemMetaInfo( void )
        {
            this->theStream = NULL;
        }

        inline ~eirFileSystemMetaInfo( void )
        {
            return;
        }

        CFile *theStream;
    };

    struct eirFileSystemWrapperProvider : public rw::customStreamInterface
    {
        void OnConstruct( rw::eStreamMode streamMode, void *userdata, void *membuf, size_t memSize ) const override
        {
            eirFileSystemMetaInfo *meta = new (membuf) eirFileSystemMetaInfo;

            meta->theStream = (CFile*)userdata;
        }

        void OnDestruct( void *memBuf, size_t memSize ) const override
        {
            eirFileSystemMetaInfo *meta = (eirFileSystemMetaInfo*)memBuf;

            meta->~eirFileSystemMetaInfo();
        }

        size_t Read( void *memBuf, void *out_buf, size_t readCount ) const override
        {
            eirFileSystemMetaInfo *meta = (eirFileSystemMetaInfo*)memBuf;

            return meta->theStream->Read( out_buf, 1, readCount );
        }

        size_t Write( void *memBuf, const void *in_buf, size_t writeCount ) const override
        {
            eirFileSystemMetaInfo *meta = (eirFileSystemMetaInfo*)memBuf;

            return meta->theStream->Write( in_buf, 1, writeCount );
        }

        void Skip( void *memBuf, rw::int64 skipCount ) const override
        {
            eirFileSystemMetaInfo *meta = (eirFileSystemMetaInfo*)memBuf;

            meta->theStream->SeekNative( skipCount, SEEK_CUR );
        }

        rw::int64 Tell( const void *memBuf ) const override
        {
            eirFileSystemMetaInfo *meta = (eirFileSystemMetaInfo*)memBuf;

            return meta->theStream->TellNative();
        }

        void Seek( void *memBuf, rw::int64 stream_offset, rw::eSeekMode seek_mode ) const override
        {
            int ansi_seek = SEEK_SET;

            if ( seek_mode == rw::RWSEEK_BEG )
            {
                ansi_seek = SEEK_SET;
            }
            else if ( seek_mode == rw::RWSEEK_CUR )
            {
                ansi_seek = SEEK_CUR;
            }
            else if ( seek_mode == rw::RWSEEK_END )
            {
                ansi_seek = SEEK_END;
            }
            else
            {
                assert( 0 );
            }

            eirFileSystemMetaInfo *meta = (eirFileSystemMetaInfo*)memBuf;

            meta->theStream->SeekNative( stream_offset, ansi_seek );
        }

        rw::int64 Size( const void *memBuf ) const override
        {
            eirFileSystemMetaInfo *meta = (eirFileSystemMetaInfo*)memBuf;

            return meta->theStream->GetSizeNative();
        }

        bool SupportsSize( const void *memBuf ) const override
        {
            return true;
        }

        CFileSystem *nativeFileSystem;
    };

    eirFileSystemWrapperProvider eirfs_file_wrap;

    rw::Stream* RwStreamCreateTranslated( rw::Interface *rwEngine, CFile *eirStream )
    {
        rw::streamConstructionCustomParam_t customParam( "eirfs_file", eirStream );

        rw::Stream *result = rwEngine->CreateStream( rw::RWSTREAMTYPE_CUSTOM, rw::RWSTREAMMODE_READWRITE, &customParam );

        return result;
    }

public:
    dispatchType& dispatch;

    CFileTranslator *outputRoot;
    CFileTranslator *resourceRoot;

    NameRegistry usedTxdNames;
    const char *txdName;
    const char *colName;
    char lodBuffer[128];

    rw::Interface *rwEngine;

    inline ResourceManager( int a, dispatchType& _dispatch ) : dispatch( _dispatch )
    {
        this->outputRoot = AcquireOutputRoot();
        this->resourceRoot = AcquireResourceRoot();

        txdName = NULL;
        colName = NULL;

        // We should use the RenderWare engine to optimize certain resources.
        const rw::LibraryVersion useRWVer = rw::KnownVersions::getGameVersion( rw::KnownVersions::SA );

        if ( rw::Interface *rwEngine = rw::CreateEngine( useRWVer ) )
        {
            // Set it up properly.
            rw::softwareMetaInfo metaInfo;
            metaInfo.applicationName = "Scene2Res";
            metaInfo.applicationVersion = "stable";
            metaInfo.description = "GTA engine map converter for MTA, made by The_GTA";

            rwEngine->SetApplicationInfo( metaInfo );
            rwEngine->SetMetaDataTagging( true );
            
            rwEngine->SetPaletteRuntime( rw::PALRUNTIME_PNGQUANT );
            rwEngine->SetDXTRuntime( rw::DXTRUNTIME_SQUISH );
            rwEngine->SetFixIncompatibleRasters( true );
            rwEngine->SetCompatTransformNativeImaging( true );
            rwEngine->SetPreferPackedSampleExport( false );
            rwEngine->SetDXTPackedDecompression( false );
            rwEngine->SetIgnoreSerializationBlockRegions( false );

            // Register the native file system wrapper type.
            eirfs_file_wrap.nativeFileSystem = fileSystem;

            rwEngine->RegisterStream( "eirfs_file", sizeof( eirFileSystemMetaInfo ), &eirfs_file_wrap );

            this->rwEngine = rwEngine;
        }
        else
        {
            this->rwEngine = NULL;
        }
    }

    inline ~ResourceManager( void )
    {
        // Delete any active resources we created.
        if ( rw::Interface *rwEngine = this->rwEngine )
        {
            rw::DeleteEngine( rwEngine );

            this->rwEngine = NULL;
        }
    }

    inline CFile* OpenOutputFile( const char *filePath, const char *mode )
    {
        CFile *outputStream = outputRoot->Open( filePath, mode );

        if ( !outputStream )
        {
            // Warn the implementation that something failed to create or open.
            printf( "warning: failed to create file handle to '%s' (using mode '%s')\n", filePath, mode );
        }

        return outputStream;
    }

    inline bool ResourceExists( const char *name, const char *outputName )
    {
        if ( outputRoot->Exists( outputName ) )
            return true;

        if ( !resourceRoot )
            return false;

        return resourceRoot->Exists( name );
    }

    inline bool _CopyOnlyIfRequired( const char *src, const char *dst )
    {
        if ( !resourceRoot )
            return false;

        if ( outputRoot->Exists( dst ) )
            return false;

        return FileSystem::FileCopy( resourceRoot, src, outputRoot, dst );
    }
    
private:
    inline rw::TexDictionary* GetTXDObject( const char *src )
    {
        rw::TexDictionary *txdObj = NULL;

        std::unique_ptr <CFile> sourceStream( resourceRoot->Open( src, "rb" ) );

        if ( sourceStream )
        {
            rw::Stream *rwStream = RwStreamCreateTranslated( rwEngine, sourceStream.get() );

            if ( rwStream )
            {
                try
                {
                    // Try to read a TXD.
                    try
                    {
                        rw::RwObject *rwobj = rwEngine->Deserialize( rwStream );

                        if ( rwobj )
                        {
                            txdObj = rw::ToTexDictionary( rwEngine, rwobj );

                            if ( !txdObj )
                            {
                                rwEngine->DeleteRwObject( rwobj );
                            }
                        }
                    }
                    catch( rw::RwException& )
                    {
                        // Ignore exceptions, we just failed.
                    }
                }
                catch( ... )
                {
                    rwEngine->DeleteStream( rwStream );

                    throw;
                }

                rwEngine->DeleteStream( rwStream );
            }
        }

        return txdObj;
    }

    inline void SaveTXDObject( const char *dst, rw::TexDictionary *txdObj )
    {
        std::unique_ptr <CFile> dstStream( outputRoot->Open( dst, "wb" ) );

        if ( dstStream )
        {
            rw::Stream *rwStream = RwStreamCreateTranslated( rwEngine, dstStream.get() );

            if ( rwStream )
            {
                try
                {
                    // Write the TXD object.
                    try
                    {
                        rwEngine->Serialize( txdObj, rwStream );
                    }
                    catch( rw::RwException& )
                    {
                        // Ignore RenderWare errors.
                    }
                }
                catch( ... )
                {
                    rwEngine->DeleteStream( rwStream );

                    throw;
                }

                rwEngine->DeleteStream( rwStream );
            }
        }
    }

    inline void OptimizeTXD( rw::TexDictionary *txdObj )
    {
        // Optimizations under-the-hood are pretty cool.

        // Adjust the version of our TXD.
        rw::LibraryVersion rwver = rwEngine->GetVersion();

        txdObj->SetEngineVersion( rwver );

        rw::TexDictionary::texIter_t iter( txdObj->GetTextureIterator() );

        for ( ; iter.IsEnd() == false; iter.Increment() )
        {
            rw::TextureBase *texHandle = iter.Resolve();

            if ( rw::Raster *texRaster = texHandle->GetRaster() )
            {
                try
                {
                    // Convert the raster to Direct3D9, for San Andreas.
                    rw::ConvertRasterTo( texRaster, "Direct3D9" );

                    // Adjust the version of our raster.
                    texRaster->SetEngineVersion( rwver );

                    // If we are not compressed, we clear all mipmaps.
                    if ( texRaster->isCompressed() == false )
                    {
                        texRaster->clearMipmaps();
                    }

                    // Then generate mipmaps, for good measure.
                    texRaster->generateMipmaps( 0xFFFFFFFF );

                    // Compress it.
                    texRaster->compress( 1.0 );

                    // Fix and improve the filtering of the texture.
                    texHandle->fixFiltering();

                    if ( adjustFiltering )
                    {
                        texHandle->improveFiltering();
                    }
                }
                catch( rw::RwException& )
                {
                    // We do not care about RenderWare exceptions.
                }
            }
        }
    }

public:
    inline bool AllocateResources( const char *name, bool lod )
    {
	    char buffer[1024];
	    char copyBuffer[1024];

	    // Copy the model file
	    _snprintf(buffer, 1023, "%s.dff", name);
	    _snprintf(copyBuffer, 1023, "models\\%s.dff", name);

	    if ( !ResourceExists(buffer, copyBuffer) )
	    {
		    printf("error: model missing (%s)\n", buffer);
		    return false;
	    }

	    if ( _CopyOnlyIfRequired(buffer, copyBuffer) )
        {
		    printf("copying model '%s'\n", name);
        }

	    if ( dispatch.requireCollision( name, lod ) )
	    {
		    // Now the collision
            const Collision *colEntry = colRegistry->FindCollisionByModel( name );

		    _snprintf(buffer, 1023, "%s.col", name);
		    _snprintf(copyBuffer, 1023, "coll\\%s.col", name);

		    if ( !colEntry && !outputRoot->Exists( copyBuffer ) )
		    {
			    if ( !lod )
			    {
				    printf("error: collision missing (%s)\n", buffer);
				    return false;
			    }

			    colName = NULL;
		    }
		    else if ( colEntry )
		    {
			    colName = name;

			    // Write collision data.
                CFile *outputStream = outputRoot->Open( copyBuffer, "wb" );

                if ( outputStream )
                {
                    colEntry->WriteToFile( outputStream );

                    delete outputStream;
                }
		    }
	    }
	    else
		    colName = NULL;

	    IDE::CObject *txdObj = GetObjectByModel(name);

	    if (!txdObj)
	    {
		    printf("could not find object def for '%s'\n", name);
		    return false;
	    }

	    if (lodSupport)
        {
		    _snprintf(lodBuffer, 127, "%.0f", txdObj->m_drawDistance);
        }
	    else
        {
		    strcpy(lodBuffer, "500");
        }

	    txdName = txdObj->m_textureName;

	    // Little hack
	    if ( !usedTxdNames.Exists( txdName ) )
	    {
		    // Copy over resources
		    _snprintf(buffer, 1023, "%s.txd", txdName);
		    _snprintf(copyBuffer, 1023, "textures\\%s.txd", txdName);

		    if ( !ResourceExists(buffer, copyBuffer) )
            {
			    printf("texture missing: %s (ignoring)\n", buffer);
            }
		    else
		    {
			    // We optimize TXD resources and stuff.
                if ( resourceRoot && outputRoot->Exists( copyBuffer ) == false )
                {
                    printf( "copying textures '%s'\n", txdName );

                    rw::TexDictionary *txdObj = GetTXDObject( buffer );

                    if ( txdObj )
                    {
                        try
                        {
                            // We want to "optimize" it for MTA.
                            // This is done by doing some things automatically under the hood.
                            // Powered by Magic.TXD.
                            OptimizeTXD( txdObj );

                            // Now write the object :)
                            SaveTXDObject( copyBuffer, txdObj );
                        }
                        catch( ... )
                        {
                            rwEngine->DeleteRwObject( txdObj );

                            throw;
                        }

                        rwEngine->DeleteRwObject( txdObj );
                    }
                    else
                    {
                        // Probably a corrupted TXD.
                        printf( "warning: corrupted txd '%s'\n", buffer );
                    }
                }

                dispatch.OnTxdEntry( txdName );
		    }

		    usedTxdNames.Add( txdName );
	    }

	    g_usedModelNames.Add( name );

        dispatch.OnModelEntry( name );

	    if ( colName )
	    {
            dispatch.OnCOLEntry( colName );
	    }

	    return true;
    }
};

template <typename processorType>
struct InstanceMapper
{
    inline InstanceMapper( processorType& proc ) : processor( proc )
    {
        processor.OnBegin();
    }

    inline ~InstanceMapper( void )
    {
        processor.OnEnd();
    }

    inline void MapInstance(
        const char *instName, unsigned int modelIndex,
        double posX, double posY, double posZ,
        double quatX, double quatY, double quatZ, double quatW,
        eInstanceVersion instVer )
    {
        // instVer is just meta-data; it does not actually say anything.

        // We need to negate the quaternion, due to convention.
        quatW = -quatW;

        double eulerX, eulerY, eulerZ;
        QuatToEuler( quatX, quatY, quatZ, quatW, eulerX, eulerY, eulerZ );

        processor.OnInstance( instName, modelIndex, posX + mapXoffset, posY + mapYoffset, posZ + mapZoffset, eulerX, eulerY, eulerZ );
    }

    processorType& processor;
};

struct InstanceProcessorDesc
{
    inline InstanceProcessorDesc( void )
    {
        mapHeader = NULL;
        mapEntry = NULL;
        mapEnd = NULL;
    }

    typedef void (*mapEntryCallback)( CFile *file, const char *instName, unsigned int modelIndex, double posX, double posY, double posZ, double eulerX, double eulerY, double eulerZ );

    const char *mapHeader;
    mapEntryCallback mapEntry;
    const char *mapEnd;
};

struct InstanceProcessor
{
    CFile *mapFile;
    InstanceProcessorDesc instDesc;

    inline InstanceProcessor( CFile *mapFile, InstanceProcessorDesc desc )
    {
        this->mapFile = mapFile;
        this->instDesc = desc;
    }

    inline void OnBegin( void )
    {
        if ( !mapFile )
            return;

        if ( instDesc.mapHeader )
        {
            StreamPrintf( mapFile, "%s", instDesc.mapHeader );
        }
    }

    inline void OnInstance( const char *instName, unsigned int modelIndex, double posX, double posY, double posZ, double eulerX, double eulerY, double eulerZ )
    {
        if ( !mapFile )
            return;

        if ( instDesc.mapEntry )
        {
            instDesc.mapEntry( mapFile, instName, modelIndex, posX, posY, posZ, eulerX, eulerY, eulerZ );
        }
    }

    inline void OnEnd( void )
    {
        if ( !mapFile )
            return;

        if ( instDesc.mapEnd )
        {
            StreamPrintf( mapFile, "%s", instDesc.mapEnd );
        }
    }
};

#endif //_RESOURCES_
